﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using log4net;
using ThreeByte.ActiveDeck.Config;
using PowerPoint = Microsoft.Office.Interop.PowerPoint;
using System.ComponentModel;
using ThreeByte.ActiveDeck.Service;
using System.Threading;
using ThreeByte.ActiveDeck.Content;
using ThreeByte.ActiveDeck.AddIn.Content.UserResponse;
using System.Xml.Linq;
using ThreeByte.ActiveDeck.Data;
using System.Windows.Threading;
using Mono.Zeroconf;
using Bonjour = Mono.Zeroconf.Providers.Bonjour;
using ThreeByte.ActiveDeck.Data.UserResponse;


namespace ThreeByte.ActiveDeck.AddIn.Config
{
    public enum ServiceStatus { Normal, Warning, Error };

    public class PresentationMonitor : INotifyPropertyChanged, IDisposable
    {
        private static ILog log = LogManager.GetLogger(typeof(PresentationMonitor));

        public event EventHandler Disposed;
        public event EventHandler<SlideConfigEventArgs> SlidePreviewChanged;
        public event EventHandler<UserResponseConfigEventArgs> UserResponsePreviewChanged;

        public CoreConfig Config { get; private set; }
        private PowerPoint.SlideShowWindow SlideShowWindow;
        private PowerPoint.Presentation SlideShowPresentation;
        public WebServiceClient Client {
            get;
            private set;
        }

        private static readonly TimeSpan NOW                = TimeSpan.FromSeconds(0);
        private static readonly TimeSpan NEVER              = TimeSpan.FromMilliseconds(-1);
        private static readonly TimeSpan POLL_INTERVAL      = TimeSpan.FromSeconds(3);
        private static readonly TimeSpan RECORD_INTERVAL    = TimeSpan.FromSeconds(30);

        //Bonjour service to broadcast existence of presentation
        Bonjour.RegisterService notificationService;

        #region Public Properties
        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(string propertyName) {
            if (PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private int _currentPowerPointSlideId;
        public int CurrentPowerPointSlideId {
            get {
                return _currentPowerPointSlideId;
            }
            private set {
                _currentPowerPointSlideId = value;
                NotifyPropertyChanged("CurrentPowerPointSlideId");
            }
        }

        private int _currentiPadSection;
        public int CurrentiPadSection {
            get {
                return _currentiPadSection;
            }
            private set {
                bool modified = (_currentiPadSection != value);
                _currentiPadSection = value;
                if(modified) {
                    NotifyPropertyChanged("CurrentiPadSection");
                    NotifyiPadPreviewChanged();
                }
            }
        }

        public DateTime PresentationStartTime {
            get;
            private set;
        }

        private int _presentationCount;
        public int PresentationCount {
            get {
                return _presentationCount;
            }
            private set {
                _presentationCount = value;
                NotifyPropertyChanged("PresentationCount");
            }
        }

        private int _presentationVersion;
        public int PresentationVersion {
            get {
                return _presentationVersion;
            }
            private set {
                _presentationVersion = value;
                NotifyPropertyChanged("PresentationVersion");
            }
        }

        public Dispatcher Dispatcher { get; private set; }

        #endregion Public Properties


        public PresentationMonitor(CoreConfig config, PowerPoint.SlideShowWindow window, Dispatcher dispatcher) {
            Config = config;
            SlideShowWindow = window;
            SlideShowPresentation = window.Presentation;
            Dispatcher = dispatcher;

            Client = new WebServiceClient(Config.PresenterLogin);
            PresentationStartTime = DateTime.Now;

            //TODO: Start the presentation record timer
            lock(_timerLock) {
                //Initialize all Timers so they are assigned, before scheduling them to begin execution
                _currentiPadStateTimer = new Timer(GetCurrentiPadState);
                _presentationVersionTimer = new Timer(GetPresentationVersion);
                _presentationStartTimer = new Timer(RecordPresentationStart);
                
                //Now schedule them to execute
                _currentiPadStateTimer.Change(NOW, NEVER);
                _presentationVersionTimer.Change(NOW, NEVER);
                _presentationStartTimer.Change(RECORD_INTERVAL, NEVER);
            }
            LoadAllUserGraphicUpdaters();
            Globals.ThisAddIn.Application.SlideShowNextSlide += new PowerPoint.EApplication_SlideShowNextSlideEventHandler(Application_SlideShowNextSlide);

            EnableNotificationService();
        }

        private void EnableNotificationService() {
            notificationService = new Bonjour.RegisterService();
            notificationService.Name = "ActiveDeck";
            notificationService.RegType = "_activedeck._tcp.";
            notificationService.ReplyDomain = "";
            notificationService.UPort = unchecked((ushort)5523);

            TxtRecord txt_record = new TxtRecord(new Bonjour.ZeroconfProvider());
            txt_record.Add("presID", Config.ID);
            notificationService.TxtRecord = txt_record;

            notificationService.Response += new RegisterServiceEventHandler(notificationService_Response);
            notificationService.AutoRename = true;
            notificationService.Register();
        }        

        void notificationService_Response(object o, RegisterServiceEventArgs args) {
            if(args.ServiceError == ServiceErrorCode.None) {
                log.Debug("Bonjour Notification registered: " + args.IsRegistered.ToString());
            } else {
                log.Error("Bonjour Notification Failed: " + args.ServiceError.ToString());
            }            
        }

        private void LoadAllUserGraphicUpdaters() {
            log.Debug("Loading Graphic Updaters");
            //Try this synchronously first

            //For each slide in the show, check to see if there are graphic elements on the slide
            foreach(PowerPoint.Slide slide in SlideShowPresentation.Slides) {
                //Get the list of all resources on this slide
                List<PowerPoint.Shape> userResponseShapes = SlideUtil.GetUserResponseGraphics(slide);
                List<UserResponseFeedbackUpdater> updaters = new List<UserResponseFeedbackUpdater>();
                
                foreach(PowerPoint.Shape urShape in userResponseShapes) {

                    int section = int.Parse(urShape.Tags["Section"]);
                    int page = int.Parse(urShape.Tags["Page"]);
                    
                    if(!(Config.ContainsId(section))) {
                        log.WarnFormat("The config object does not contain any information about section {0}", section);
                    } else {
                        if(urShape.Tags["isGroup"] == "true") {
                            string configs = urShape.Tags["Pages"];
                            if(configs != "") {
                                string[] allConfigs = configs.Split('|');

                                List<MultipleChoiceConfig> mcConfigs = new List<MultipleChoiceConfig>();

                                Dictionary<int, int> pages = new Dictionary<int, int>();

                                GroupedUserResponseResultsData groupData;

                                foreach(string c in allConfigs) {
                                    string[] sectionAndPage = c.Split(',');
                                    MultipleChoiceConfig mcConfig = (MultipleChoiceConfig)Config[int.Parse(sectionAndPage[0])][int.Parse(sectionAndPage[1])];
                                    mcConfigs.Add(mcConfig);

                                    if(mcConfig.HasAnswer) {
                                        for(int i = 0; i < mcConfig.Answers.Count; i++) {
                                            if(mcConfig.Answers[i].IsCorrect) {
                                                pages.Add(mcConfig.PageId, i + 1);
                                            }
                                        }
                                    }
                                }

                                //WebServiceClient client = new WebServiceClient(cConfig.PresenterLogin);
                                XElement data = Client.GetMultiQuestionGroupedUserData(Config.ID, pages);

                                groupData = GroupedUserResponseResultsData.ParseXml(data); 

                                GroupedChartUpdater updater = new GroupedChartUpdater(Config.ID, urShape, Client, groupData);
                                updaters.Add(updater);
                            }
                        } else {
                            string configType = urShape.Tags["ConfigType"];
                            if(configType == typeof(MultipleChoiceConfig).ToString()) {
                                MultipleChoiceConfig config = (MultipleChoiceConfig)Config[section][page];
                                if(config == null) {
                                    log.WarnFormat("Cannot locate multiple choice associated with this feedback shape: {0}/{1}", section, page);
                                } else {
                                    ChartUpdater updater = new ChartUpdater(Config.ID, urShape, Client, config, Dispatcher);
                                    updaters.Add(updater);
                                }
                            } else if(configType == typeof(QuestionConfig).ToString()) {
                                QuestionConfig config = (QuestionConfig)Config[section][page];
                                if(config == null) {
                                    log.WarnFormat("Cannot locate question associated with this feedback shape: {0}/{1}", section, page);
                                } else {
                                    AnimatedTextUpdater updater = new AnimatedTextUpdater(Config.ID, urShape, Client, config);
                                    updaters.Add(updater);
                                }
                            } else if(configType == typeof(CategoriesConfig).ToString()) {
                                CategoriesConfig config = (CategoriesConfig)Config[section][page];
                                if(config == null) {
                                    log.WarnFormat("Cannot locate categories associated with this feedback shape: {0}/{1}", section, page);
                                } else {
                                    CategoriesUpdater updater = new CategoriesUpdater(Config.ID, urShape, Client, config, Dispatcher);
                                    updaters.Add(updater);
                                }
                            } else {
                                //TODO: Handle other question types...
                            }
                        }
                    }
                }
                //Assign the list of updaters
                if(updaters.Count > 0) {
                    _userGraphicUpdaters[slide.SlideID] = updaters;
                }

            }
        }

        private Dictionary<int, List<UserResponseFeedbackUpdater>> _userGraphicUpdaters = new Dictionary<int, List<UserResponseFeedbackUpdater>>();
        //private List<UserResponseFeedbackUpdater> _userGraphicUpdaters = new List<UserResponseFeedbackUpdater>();

        //TODO: Consider offloading this to the background thread
        private void Application_SlideShowNextSlide(PowerPoint.SlideShowWindow Wn) {
            //Make sure that this event is targeting this presentation
            if (Wn != SlideShowWindow || _disposed) {
                return; //Short circuit and don't interfere with presentations that don't have anything configured
            }
            
            //Update Interactive State
            CurrentPowerPointSlideId = Wn.View.Slide.SlideID;
            
            int nextSlideNumber = -1;
            if(Wn.View.Slide.SlideNumber < Wn.Presentation.Slides.Count)
                nextSlideNumber = Wn.View.Slide.SlideNumber + 1;

            int nextSlideId = -1;
            foreach (PowerPoint.Slide s in Wn.Presentation.Slides) {
                if (s.SlideNumber == nextSlideNumber) {
                    nextSlideId = s.SlideID;                    
                }               
            }

            log.DebugFormat("NextSlide: {0}", CurrentPowerPointSlideId);

            if(Config.ContainsId(CurrentPowerPointSlideId)){
                SlideConfig slideConfig = Config[CurrentPowerPointSlideId];

                UpdatePresentationState(slideConfig.SlideId);
            }

            //Enable any user response updaters for this slide
            //Disable any other user response updaters
            UserResponseFeedbackUpdater firstUpdater = null;
            foreach(int i in _userGraphicUpdaters.Keys) {
                bool active = (i == CurrentPowerPointSlideId) || (i == nextSlideId);
                foreach(UserResponseFeedbackUpdater u in _userGraphicUpdaters[i]) {
                    u.Active = active;
                    if(active && (firstUpdater == null)) {
                        firstUpdater = u;
                    }
                }
            }
            RegisterUserResponseUpdater(firstUpdater);
            

        }


        //Must handle this asynchronously and handle all errors
        private void UpdatePresentationState(int slideId) {

            ThreadPool.QueueUserWorkItem(UpdatePresentationStateCallback, slideId);

        }

        private void UpdatePresentationStateCallback(object state) {
            Client.SetPresentationState(Config.ID, (int)state); //Does this on a background thread
            //TODO:  Do this better and monitor it
        }

        #region Communication Timers
        private Timer _currentiPadStateTimer;
        private Timer _presentationVersionTimer;
        private Timer _presentationStartTimer;

        private void GetCurrentiPadState(object state){

            if(_disposed) {
                return;
            }
            UpdateTimer(_currentiPadStateTimer, NEVER, NEVER);
            log.Debug("Polling for iPad state");

            try {
                XElement sectionNode = Client.GetPresentationState(Config.ID);
                CurrentiPadSection = int.Parse(sectionNode.Attribute("Section").Value);
            } catch(Exception ex) {
                log.Error("Problem polling for current iPad slide", ex);
            }
            UpdateTimer(_currentiPadStateTimer, POLL_INTERVAL, NEVER);
        }

        private void GetPresentationVersion(object state) {
            if(_disposed) {
                return;
            }
            UpdateTimer(_presentationVersionTimer, NEVER, NEVER);

            log.Debug("Polling for Presentation Header");

            try {
                PresentationHeader header = Client.GetPresentationInfo(Config.ID);
                PresentationVersion = header.Version;
            } catch(Exception ex) {
                log.Error("Problem polling for Presentation Header", ex);
                //Poll again...
                UpdateTimer(_presentationVersionTimer, POLL_INTERVAL, NEVER);
            }
        }

        private void RecordPresentationStart(object state) {
            if(_disposed) {
                return;
            }
            UpdateTimer(_presentationStartTimer, NEVER, NEVER);

            try {
                PresentationHeader header = Client.RecordPresentationStart(Config.ID);
                PresentationCount = header.PresentationCount;
            } catch(Exception ex) {
                log.Error("Problem recording presentation start", ex);
                //Poll again...
                UpdateTimer(_presentationStartTimer, POLL_INTERVAL, NEVER);
            }
        }

        private readonly object _timerLock = new object();

        private void UpdateTimer(System.Threading.Timer timer, TimeSpan dueTime, TimeSpan period) {
            lock(_timerLock) {
                if(_disposed) {
                    return;
                }
                timer.Change(dueTime, period);
            }
        }

        #endregion Communication Timers

        private void NotifyiPadPreviewChanged() {
            log.Info("Update slide Preview");

            SlideConfig slideConfig = null;
            //See if there is a slide config for this, and then generate the preview
            if(Config.ContainsId(CurrentiPadSection)) {
                slideConfig = Config[CurrentiPadSection];   
            }
            if(SlidePreviewChanged != null) {
                SlidePreviewChanged(this, new SlideConfigEventArgs(slideConfig));
            }
        }

        private void RegisterUserResponseUpdater(UserResponseFeedbackUpdater updater = null) {
            log.Info("Update user response graphic placeholder");
            if(updater != null) {
                //_userGraphicUpdaters.Add(updater);
            }

            if(UserResponsePreviewChanged != null) {
                UserResponsePreviewChanged(this, new UserResponseConfigEventArgs(updater));
            }
        }


        private bool _disposed = false;
        public void Dispose() {
            if (_disposed) {
                throw new ObjectDisposedException("Cannot dispose a PresentationMonitor object more than once");
            }
            _disposed = true;

            Globals.ThisAddIn.Application.SlideShowNextSlide -= Application_SlideShowNextSlide;

            lock(_timerLock) {
                _currentiPadStateTimer.Change(NEVER, NEVER);
                _currentiPadStateTimer.Dispose();
                _presentationVersionTimer.Dispose();
                _presentationStartTimer.Dispose();
            }

            foreach(int id in _userGraphicUpdaters.Keys) {
                foreach(UserResponseFeedbackUpdater f in _userGraphicUpdaters[id]) {
                    f.Dispose();
                }
            }
            _userGraphicUpdaters.Clear();

            notificationService.Dispose();
            notificationService.TxtRecord.Dispose();

            if(Disposed != null) {
                Disposed(this, EventArgs.Empty);
            }
        }

    }

    public class SlideConfigEventArgs : EventArgs
    {
        public SlideConfig SlideConfig { get; private set; }

        public SlideConfigEventArgs(SlideConfig slideConfig) {
            SlideConfig = slideConfig;
        }
    }

    public class UserResponseConfigEventArgs : EventArgs
    {
        public UserResponseFeedbackUpdater Updater { get; private set; }

        public UserResponseConfigEventArgs(UserResponseFeedbackUpdater updater) {
            Updater = updater;
        }
    }

}
